/*
 * Copyright (c) 2009, Jan Pastor. All Rights Reserved.
 */

package sk.jp.jitag.dao.db4o;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.db4o.Db4o;
import com.db4o.ObjectContainer;
import com.db4o.ObjectSet;
import com.db4o.config.Configuration;
import com.db4o.constraints.UniqueFieldValueConstraint;
import com.db4o.ext.ExtObjectContainer;
import com.db4o.ext.ExtObjectServer;
import com.db4o.ta.RollbackStrategy;
import com.db4o.ta.TransparentPersistenceSupport;

import sk.jp.commons.exception.ApplicationException;
import sk.jp.commons.utils.ExceptionUtils;
import sk.jp.commons.utils.LogMsg;
import sk.jp.jitag.ConfigManager;
import sk.jp.jitag.dao.DataSource;
import sk.jp.jitag.dao.DataSourceProvider;
import sk.jp.jitag.model.DbVersion;
import sk.jp.jitag.util.Defaults;
import sk.jp.jitag.util.Property;

@Component
public class Db4oDataSourceProvider implements DataSourceProvider {

    private static final Logger log = LoggerFactory.getLogger(Db4oDataSourceProvider.class);

    @Autowired
    private ConfigManager configManager;

    private String dbFileName = ConfigManager.CONFIG_FILE;
    private List<String> configuredEntities = new ArrayList<String>(0);
    private int debugLevel = 0;

    private ExtObjectServer db4oServer = null;

    //----------------------------------------------------------------------

    /**
     * Set database file name.
     * @param dbFileName
     */
    public void setDbFileName(String dbFileName) {
        this.dbFileName = dbFileName;
    }

    /**
     * Set a list of entities that has to be additionaly configured
     * (processig annotations for fields such as {@link Unique}).
     * @param configuredEntities
     */
    public void setConfiguredEntities(List<String> configuredEntities) {
        this.configuredEntities = configuredEntities;
    }

    /**
     * Set up Db4o engine debug level.
     * @param debugLevel integer from 0 to 3, where:<br>
     * Level 0 - no messages<br>
     * Level 1 - open and close messages<br>
     * Level 2 - messages for new, update and delete<br>
     * Level 3 - messages for activate and deactivate.<br>
     * Messages will be output to the configured std output.
     * See {@link Configuration#messageLevel(int)} 
     * method documentation for details.
     */
    public void setDebugLevel(int debugLevel) {
        this.debugLevel = debugLevel;
    }

    //----------------------------------------------------------------------

    public synchronized void initLazy() {
        log.info("Initializing database server connection...");
        try {
            Configuration configuration = Db4o.newConfiguration();
            configure(configuration);
            String dbFilePath = getDbFilePath();
            db4oServer = Db4o.openServer(configuration, dbFilePath, 0).ext();

            try {
                checkDbVersion();
            }catch(ApplicationException aeCheckedVersion){
                close();
                throw aeCheckedVersion;
            }catch(Throwable eCheckVersion){
                close();
                throw new ApplicationException("Can't check database version compatibility, database will not be accessible",
                                               "sk.jp.jitag.dao.db4o.Db4oDataSourceProvider.err.001",
                                               null, eCheckVersion);
            }
        }catch(ApplicationException ae){
            throw ae;
        }catch(Throwable e){
            throw new ApplicationException("Can't initialize database server connection",
                                           "sk.jp.jitag.dao.db4o.Db4oDataSourceProvider.err.002",
                                           ExceptionUtils.paramsToMap("dbFileName", dbFileName),
                                           e);
        }
    }

    /**
     * Doesn't throws exception. If already closed, or not initialized before,
     * does nothing.
     */
    public synchronized void close() {
        log.debug("Closing database server connection...");
        if(db4oServer == null){
            log.warn("Database server connection already closed");
            return;
        }
        try {
            if(db4oServer.clientCount() > 0){
                log.debug(LogMsg.toString("Count of connected clients before closing database server",
                                          "count", db4oServer.clientCount()));
            }

            db4oServer.close();
            db4oServer = null;
            log.info("Database server connection closed.");
        } catch (Throwable e) {
            log.error("Can't close database server connection", e);
        }
    }

    public synchronized DataSource getDataSource() {
        log.debug(LogMsg.inp());
        try {
            checkIfInitialized();
        } catch (IllegalStateException ignored) {
            initLazy();
        }
        ExtObjectContainer db4oClient = db4oServer.openClient().ext();
        return new Db4oDataSource(db4oClient);
    }

    public synchronized void backup(String newDbPath){
        log.debug(LogMsg.inp("newDbPath", newDbPath));
        try {
            checkIfInitialized();
        } catch (IllegalStateException ignored) {
            initLazy();
        }
        try {
            db4oServer.backup(newDbPath);
        } catch (Exception e) {
            throw new ApplicationException("Can't create hot-backup of database file",
                                           "",  //TODO Dokoncit!!! code
                                           ExceptionUtils.paramsToMap("newDbPath", newDbPath),
                                           e);
        }
    }

    //----------------------------------------------------------------------

    String getDbFilePath(){
        return configManager.getConfigDir().getAbsolutePath() + "/" + dbFileName;
    }

    /**
     * Checks if server connection is created and provider is ready to work.
     * @throws IllegalStateException if provider is not already initialized.
     */
    private void checkIfInitialized(){
        if(db4oServer == null){
            throw new IllegalStateException("Server connection is not initialized");
        }
    }

    /**
     * Performs entity-specific configuration for each entity declared as configurable
     * in list property {@link #configuredEntities}.
     * @param configuration
     */
    private void configure(Configuration configuration){
        //Configure container
        log.debug("Configuring database container...");
        configuration.messageLevel(debugLevel);
        configuration.setOut(System.out); 
        configuration.add(new TransparentPersistenceSupport(new RollbackStrategy() {
            public void rollback(ObjectContainer container, Object obj) {
                container.ext().deactivate(obj);
            }
        }));
//        configuration.detectSchemaChanges(false);  THIS MUST BE COMMENTED!

        //Configure entities
        ClassLoader classLoader = getClass().getClassLoader();
        for (String configuredEntity : configuredEntities) {
            log.debug("Configuring entity "+configuredEntity+"...");
            Class<?> entityClass;
            try {
                entityClass = classLoader.loadClass(configuredEntity);
            } catch (ClassNotFoundException e) {
                throw new ApplicationException("Can't load class specified as configured entity",
                                               "sk.jp.jitag.dao.db4o.Db4oDataSourceProvider.err.003",
                                               ExceptionUtils.paramsToMap("class", configuredEntity),
                                               e);
            }

            //Configure entity fields
            Field[] fields = entityClass.getDeclaredFields();
            for (Field field : fields) {
                //Set up unique constraint
                String fieldName = field.getName();
                if(field.isAnnotationPresent(Unique.class)){
                    log.debug(" - configuring unique constraint for field "+ fieldName);
                    configuration.add(new UniqueFieldValueConstraint(entityClass, fieldName));
                }
                //Set up cascade On Update
                if(field.isAnnotationPresent(CascadeOnDelete.class)){
                    log.debug(" - configuring cascade on delete behaviour for field "+ fieldName);
                    configuration.objectClass(entityClass).objectField(fieldName).cascadeOnDelete(true);
                }
            }
        }
        log.info("Database successfully configured");
    }

    /**
     * Check database file compatibility with app logic.
     * <p>If not compatible, throws exception.
     */
    private void checkDbVersion() {
        Integer currAppDbVersion = Property.appVersionDb.intValue();
        Integer currDbVersion = getDbVersion();

        List<Integer> supportedDbVers = Defaults.dbVersionCompatibility.get(currAppDbVersion);
        if(!supportedDbVers.contains(currDbVersion)){
            throw new ApplicationException("Current version of application doesn't support existing database. Try to reinstall application to older version or mannualy delete database files.",
                                           "sk.jp.jitag.dao.db4o.Db4oDataSourceProvider.err.004",
                       ExceptionUtils.paramsToMap("currAppDbVersion", currAppDbVersion,
                                                  "currDbVersion", currDbVersion));
        }
    }

    /**
     * Retrieve database version number from database.
     * <p>If database is empty, set-up and store current database version.
     * @return database version number.
     */
    private Integer getDbVersion(){
        Integer currAppDbVersion = Property.appVersionDb.intValue();
        Integer versionNumber = null;
        DataSource ds = getDataSource();
        ExtObjectContainer db4oClient = (ExtObjectContainer) ds.getUnderlyingDataSource();
        try {
            ObjectSet<DbVersion> dbVersionSet = db4oClient.query(DbVersion.class);
            if(dbVersionSet.hasNext()){
            DbVersion dbVersion = dbVersionSet.next();
                versionNumber = dbVersion.getVersion();
            }
            if(versionNumber == null){
                //If empty database
                versionNumber = currAppDbVersion;
                db4oClient.store(new DbVersion(currAppDbVersion));
            }
            db4oClient.commit();
        } finally {
            db4oClient.close();
        }
        return versionNumber;
    }

}
